/*
 * NPlot - A charting library for .NET
 * 
 * LinearAxis.cs
 * Copyright (C) 2003-2006 Matt Howlett and others.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *	  list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *	  this list of conditions and the following disclaimer in the documentation
 *	  and/or other materials provided with the distribution.
 * 3. Neither the name of NPlot nor the names of its contributors may
 *	  be used to endorse or promote products derived from this software without
 *	  specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System.Drawing;
using System.Collections;
using System;
using System.Text;
using System.Diagnostics;

namespace NPlot
{

	/// <summary>
	/// Provides functionality for drawing axes with a linear numeric scale.
	/// </summary>
	public class LinearAxis : Axis, System.ICloneable
	{

		/// <summary>
		/// Deep copy of LinearAxis.
		/// </summary>
		/// <returns>A copy of the LinearAxis Class</returns>
		public override object Clone()
		{
			LinearAxis a = new LinearAxis();
			// ensure that this isn't being called on a derived type. If it is, then oh no!
			if (this.GetType() != a.GetType())
			{
				throw new NPlotException( "Clone not defined in derived type. Help!" );
			}
			this.DoClone( this, a );
			return a;
		}


		/// <summary>
		/// Helper method for Clone.
		/// </summary>
		protected void DoClone( LinearAxis b, LinearAxis a )
		{
			Axis.DoClone( b, a );

			a.numberSmallTicks_ = b.numberSmallTicks_;
			a.largeTickValue_ = b.largeTickValue_;
			a.largeTickStep_ = b.largeTickStep_;

			a.offset_ = b.offset_;
			a.scale_ = b.scale_;
		}


		/// <summary>
		/// Copy constructor
		/// </summary>
		/// <param name="a">The Axis to clone</param>
		public LinearAxis( Axis a )
			: base( a )
		{
			Init();
		}


		/// <summary>
		/// Default constructor.
		/// </summary>
		public LinearAxis()
			: base()
		{
			Init();
		}


		/// <summary>
		/// Construct a linear axis with the provided world min and max values.
		/// </summary>
		/// <param name="worldMin">the world minimum value of the axis.</param>
		/// <param name="worldMax">the world maximum value of the axis.</param>
		public LinearAxis( double worldMin, double worldMax )
			: base( worldMin, worldMax )
		{
			Init();
		}


		private void Init() 
		{
			this.NumberFormat = "{0:g5}";
		}


		/// <summary>
		/// Draws the large and small ticks [and tick labels] for this axis.
		/// </summary>
		/// <param name="g">The graphics surface on which to draw.</param>
		/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
		/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
		/// <param name="boundingBox">out: smallest box that completely surrounds all ticks and associated labels for this axis.</param>
		/// <param name="labelOffset">out: offset from the axis to draw the axis label.</param>
		protected override void DrawTicks(
			Graphics g, 
			Point physicalMin, 
			Point physicalMax, 
			out object labelOffset,
			out object boundingBox )
		{

			Point tLabelOffset;
			Rectangle tBoundingBox;

			labelOffset = this.getDefaultLabelOffset( physicalMin, physicalMax );
			boundingBox = null;

			ArrayList largeTickPositions;
			ArrayList smallTickPositions;
			this.WorldTickPositions( physicalMin, physicalMax, out largeTickPositions, out smallTickPositions );

			labelOffset = new Point( 0, 0 );
			boundingBox = null;

			if (largeTickPositions.Count > 0)
			{
				for (int i = 0; i < largeTickPositions.Count; ++i)
				{
					double labelNumber = (double)largeTickPositions[i];

					// TODO: Find out why zero is sometimes significantly not zero [seen as high as 10^-16].
					if (Math.Abs(labelNumber) < 0.000000000000001)
					{
						labelNumber = 0.0;
					}

					StringBuilder label = new StringBuilder();
					label.AppendFormat(this.NumberFormat, labelNumber);

					this.DrawTick( g, ((double)largeTickPositions[i]/this.scale_-this.offset_), 
						this.LargeTickSize, label.ToString(),
						new Point(0,0), physicalMin, physicalMax, 
						out tLabelOffset, out tBoundingBox );
					
					Axis.UpdateOffsetAndBounds( ref labelOffset, ref boundingBox, 
						tLabelOffset, tBoundingBox );
				}
			}

			for (int i = 0; i<smallTickPositions.Count; ++i)
			{
				this.DrawTick( g, ((double)smallTickPositions[i]/this.scale_-this.offset_), 
					this.SmallTickSize, "", 
					new Point(0, 0), physicalMin, physicalMax, 
					out tLabelOffset, out tBoundingBox );

				// assume bounding box and label offset unchanged by small tick bounds.
			}
		}


		/// <summary>
		/// Determines the positions, in world coordinates, of the small ticks
		/// if they have not already been generated.
		/// 
		/// </summary>
		/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
		/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
		/// <param name="largeTickPositions">The positions of the large ticks.</param>
		/// <param name="smallTickPositions">If null, small tick positions are returned via this parameter. Otherwise this function does nothing.</param>
		internal override void WorldTickPositions_SecondPass( 
			Point physicalMin,
			Point physicalMax,
			ArrayList largeTickPositions, 
			ref ArrayList smallTickPositions )
		{

			// return if already generated.
			if (smallTickPositions != null)
				return;

			int physicalAxisLength = Utils.Distance( physicalMin, physicalMax );

			double adjustedMax = this.AdjustedWorldValue( WorldMax );
			double adjustedMin = this.AdjustedWorldValue( WorldMin );

			smallTickPositions = new ArrayList();

			// TODO: Can optimize this now.
			bool shouldCullMiddle;
			double bigTickSpacing = this.DetermineLargeTickStep( physicalAxisLength, out shouldCullMiddle );

			int nSmall = this.DetermineNumberSmallTicks( bigTickSpacing );
			double smallTickSpacing = bigTickSpacing / (double)nSmall;

			// if there is at least one big tick
			if (largeTickPositions.Count > 0)
			{
				double pos1 = (double)largeTickPositions[0] - smallTickSpacing;
				while (pos1 > adjustedMin)
				{
					smallTickPositions.Add( pos1 );
					pos1 -= smallTickSpacing;
				}
			}

			for (int i = 0; i < largeTickPositions.Count; ++i )
			{
				for (int j = 1; j < nSmall; ++j )
				{
					double pos = (double)largeTickPositions[i] + ((double)j) * smallTickSpacing;
					if (pos <= adjustedMax)
					{
						smallTickPositions.Add( pos );
					}
				}
			}
		}


		/// <summary>
		/// Adjusts a real world value to one that has been modified to
		/// reflect the Axis Scale and Offset properties.
		/// </summary>
		/// <param name="world">world value to adjust</param>
		/// <returns>adjusted world value</returns>
		public double AdjustedWorldValue( double world )
		{
			return world * this.scale_ + this.offset_;
		}


		/// <summary>
		/// Determines the positions, in world coordinates, of the large ticks. 
		/// When the physical extent of the axis is small, some of the positions 
		/// that were generated in this pass may be converted to small tick 
		/// positions and returned as well.
		///
		/// If the LargeTickStep isn't set then this is calculated automatically and
		/// depends on the physical extent of the axis. 
		/// </summary>
		/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
		/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
		/// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
		/// <param name="smallTickPositions">ArrayList containing the positions of the small ticks if calculated, null otherwise.</param>
		internal override void WorldTickPositions_FirstPass(
			Point physicalMin, 
			Point physicalMax,
			out ArrayList largeTickPositions,
			out ArrayList smallTickPositions
			)
		{

			// (1) error check

			if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
			{
				throw new NPlotException( "world extent of axis not set." );
			}
			
			double adjustedMax = this.AdjustedWorldValue( WorldMax );
			double adjustedMin = this.AdjustedWorldValue( WorldMin );

			// (2) determine distance between large ticks.
			bool shouldCullMiddle;
			double tickDist = this.DetermineLargeTickStep( 
				Utils.Distance(physicalMin, physicalMax),
				out shouldCullMiddle );

			// (3) determine starting position.
			double first = 0.0f;

			if (!double.IsNaN(largeTickValue_)) 
			{
				// this works for both case when largTickValue_ lt or gt adjustedMin.
				first = largeTickValue_ + (Math.Ceiling((adjustedMin-largeTickValue_)/tickDist))*tickDist;
			}
			else
			{
				if( adjustedMin > 0.0 )
				{
					double nToFirst = Math.Floor(adjustedMin / tickDist) + 1.0f;
					first = nToFirst * tickDist;
				}
				else
				{
					double nToFirst = Math.Floor(-adjustedMin/tickDist) - 1.0f;
					first = -nToFirst * tickDist;
				}

				// could miss one, if first is just below zero.
				if ((first - tickDist) >= adjustedMin)
				{
					first -= tickDist;
				}
			}

			// (4) now make list of large tick positions.
			largeTickPositions = new ArrayList();

			if (tickDist < 0.0) // some sanity checking. TODO: remove this.
				throw new NPlotException( "Tick dist is negative" );

			double position = first;
			int safetyCount = 0;
			while (
				(position <= adjustedMax) && 
				(++safetyCount < 5000) )
			{
				largeTickPositions.Add( position );
				position += tickDist;
			}

			// (5) if the physical extent is too small, and the middle 
			// ticks should be turned into small ticks, then do this now.
			smallTickPositions = null;
			if (shouldCullMiddle)
			{
				smallTickPositions = new ArrayList();

				if (largeTickPositions.Count > 2)
				{
					for (int i=1; i<largeTickPositions.Count-1; ++i)
					{
						smallTickPositions.Add( largeTickPositions[i] );
					}
				}

				ArrayList culledPositions = new ArrayList();
				culledPositions.Add( largeTickPositions[0] );
				culledPositions.Add( largeTickPositions[largeTickPositions.Count-1] );
				largeTickPositions = culledPositions;
			}
		}


		/// <summary>
		/// Calculates the world spacing between large ticks, based on the physical
		/// axis length (parameter), world axis length, Mantissa values and 
		/// MinPhysicalLargeTickStep. A value such that at least two 
		/// </summary>
		/// <param name="physicalLength">physical length of the axis</param>
		/// <param name="shouldCullMiddle">Returns true if we were forced to make spacing of 
		/// large ticks too small in order to ensure that there are at least two of 
		/// them. The draw ticks method should not draw more than two large ticks if this
		/// returns true.</param>
		/// <returns>Large tick spacing</returns>
		/// <remarks>TODO: This can be optimised a bit.</remarks>
		private double DetermineLargeTickStep( float physicalLength, out bool shouldCullMiddle )
		{
			shouldCullMiddle = false;

			if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
			{
				throw new NPlotException( "world extent of axis not set." );
			}

			// if the large tick has been explicitly set, then return this.
			if ( !double.IsNaN(largeTickStep_) )
			{
				if ( largeTickStep_ <= 0.0f )
				{
					throw new NPlotException( 
						"can't have negative or zero tick step - reverse WorldMin WorldMax instead."
					);
				}
				return largeTickStep_;
			}

			// otherwise we need to calculate the large tick step ourselves.

			// adjust world max and min for offset and scale properties of axis.
			double adjustedMax = this.AdjustedWorldValue( WorldMax );
			double adjustedMin = this.AdjustedWorldValue( WorldMin );
			double range = adjustedMax - adjustedMin;

			// if axis has zero world length, then return arbitrary number.
			if ( Utils.DoubleEqual( adjustedMax, adjustedMin ) )
			{
				return 1.0f;
			}

			double approxTickStep;
			if (TicksIndependentOfPhysicalExtent)
			{
				approxTickStep = range / 6.0f;
			}
			else
			{
				approxTickStep = (MinPhysicalLargeTickStep / physicalLength) * range;
			}

			double exponent = Math.Floor( Math.Log10( approxTickStep ) );
			double mantissa = Math.Pow( 10.0, Math.Log10( approxTickStep ) - exponent );

			// determine next whole mantissa below the approx one.
			int mantissaIndex = Mantissas.Length-1;
			for (int i=1; i<Mantissas.Length; ++i)
			{
				if (mantissa < Mantissas[i])
				{
					mantissaIndex = i-1;
					break;
				}
			}
			
			// then choose next largest spacing. 
			mantissaIndex += 1;
			if (mantissaIndex == Mantissas.Length)
			{
				mantissaIndex = 0;
				exponent += 1.0;
			}

			if (!TicksIndependentOfPhysicalExtent)
			{
				// now make sure that the returned value is such that at least two 
				// large tick marks will be displayed.
				double tickStep = Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
				float physicalStep = (float)((tickStep / range) * physicalLength);

				while (physicalStep > physicalLength/2)
				{
					shouldCullMiddle = true;

					mantissaIndex -= 1;
					if (mantissaIndex == -1)
					{
						mantissaIndex = Mantissas.Length-1;
						exponent -= 1.0;
					}

					tickStep = Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
					physicalStep = (float)((tickStep / range) * physicalLength);
				}
			}

			// and we're done.
			return Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
		}


		/// <summary>
		/// Given the large tick step, determine the number of small ticks that should
		/// be placed in between.
		/// </summary>
		/// <param name="bigTickDist">the large tick step.</param>
		/// <returns>the number of small ticks to place between large ticks.</returns>
		private int DetermineNumberSmallTicks( double bigTickDist )
		{
			if (this.numberSmallTicks_ != null)
			{
				return (int)this.numberSmallTicks_+1;
			}

			if (this.SmallTickCounts.Length != this.Mantissas.Length)
			{
				throw new NPlotException( "Mantissa.Length != SmallTickCounts.Length" );
			}

			if (bigTickDist > 0.0f)
			{

				double exponent = Math.Floor( Math.Log10( bigTickDist ) );
				double mantissa = Math.Pow( 10.0, Math.Log10( bigTickDist ) - exponent );

				for (int i=0; i<Mantissas.Length; ++i)
				{
					if ( Math.Abs(mantissa-Mantissas[i]) < 0.001 )
					{
						return SmallTickCounts[i]+1;
					}
				}
			}
				
			return 0;
		}


		/// <summary>
		/// The distance between large ticks. If this is set to NaN [default],
		/// this distance will be calculated automatically.
		/// </summary>
		public double LargeTickStep
		{
			set
			{
				largeTickStep_ = value;
			}
			get
			{
				return largeTickStep_;
			}
		}
		/// <summary>
		/// If set !NaN, gives the distance between large ticks.
		/// </summary>
		private double largeTickStep_ = double.NaN;


		/// <summary>
		/// If set, a large tick will be placed at this position, and other large ticks will 
		/// be placed relative to this position.
		/// </summary>
		public double LargeTickValue
		{
			set
			{
				largeTickValue_ = value;
			}
			get
			{
				return largeTickValue_;
			}
		}
		private double largeTickValue_ = double.NaN;


		/// <summary>
		/// The number of small ticks between large ticks.
		/// </summary>
		public int NumberOfSmallTicks
		{
			set
			{
				numberSmallTicks_ = value;
			}
			get
			{
				// TODO: something better here.
				return (int)numberSmallTicks_;
			}
		}
		private object numberSmallTicks_ = null;


		/// <summary>
		/// Scale to apply to world values when labelling axis:
		/// (labelWorld = world * scale + offset). This does not
		/// affect the "real" world range of the axis. 
		/// </summary>
		public double Scale
		{
			get
			{
				return scale_;
			}
			set
			{
				scale_ = value;
			}
		}


		/// <summary>
		/// Offset to apply to world values when labelling the axis:
		/// (labelWorld = axisWorld * scale + offset). This does not
		/// affect the "real" world range of the axis.
		/// </summary>
		public double Offset
		{
			get
			{
				return offset_;
			}
			set
			{
				offset_ = value;
			}
		}


		/// <summary>
		/// If LargeTickStep isn't specified, then a suitable value is 
		/// calculated automatically. To determine the tick spacing, the
		/// world axis length is divided by ApproximateNumberLargeTicks
		/// and the next lowest distance m*10^e for some m in the Mantissas
		/// set and some integer e is used as the large tick spacing. 
		/// </summary>
		public float ApproxNumberLargeTicks = 3.0f;


		/// <summary>
		/// If LargeTickStep isn't specified, then a suitable value is
		/// calculated automatically. The value will be of the form
		/// m*10^e for some m in this set.
		/// </summary>
		public double[] Mantissas = {1.0, 2.0, 5.0};


		/// <summary>
		/// If NumberOfSmallTicks isn't specified then .... 
		/// If specified LargeTickStep manually, then no small ticks unless
		/// NumberOfSmallTicks specified.
		/// </summary>
		public int[] SmallTickCounts = {4, 1, 4};


		private double offset_ = 0.0;

		private double scale_ = 1.0;
	}
}
